<?php
// +-------------------------------------------------------------------------+
// | PHP Version 5                                                           |
// +-------------------------------------------------------------------------+
// | ImaEngine - web development framework                                   |
// | Copyright (c) 2006-2007 Sardar Yumatov <ja.doma@gmail.com>              |
// +-------------------------------------------------------------------------+
// | This library is free software; you can redistribute it and/or modify it |
// | under the terms of the GNU Lesser General Public License as published   |
// | by the Free Software Foundation; either version 2.1 of the License, or  |
// | (at your option) any later version.                                     |
// |                                                                         |
// | This library is distributed in the hope that it will be useful, but     |
// | WITHOUT ANY WARRANTY; without even the implied warranty of              |
// | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.                    |
// | See the GNU Lesser General Public License for more details.             |
// |                                                                         |
// | You should have received a copy of the GNU Lesser General Public        |
// | License along with this library; if not, write to the Free Software     |
// | Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA |
// +-------------------------------------------------------------------------+

/**
* This file contains abstract ActiveRecord design pattern implementation.
*
* @package common.db
* @category ActiveRecord
* @version v1.0
* @author Sardar Yumatov <ja.doma@gmail.com>
* @copyright Copyright &copy; 2006-2007 Sardar Yumatov
*
* @PHP5only
*/


if(!defined('PROCESS')) {
	header('HTTP/1.1 404 Not Found');
	die('NotFound');
}

require_once('lib/db/DatabaseExceptions.php');

/**
 * Abstract active record design pattern implementation.
 *
 * The design pattern allows accessing fields of a row in a database
 * as properties of the object. Reading or writing to such property
 * will return or update data in the database per row.
 *
 * This class doesn't handle actual fetch or update of data, instead
 * it collects updates made to the object. The subclasses should
 * define which fields are accesable and updateable, and provide
 * update implementation. The subclasses may collect all updates
 * to a single "parent" transaction if a whole bulk of rows needs to be changed.
 * However all massive changes should be better implemented by
 * subclasses using direct SQL.
 *
 * Your active record object will probably extend some subclass
 * of this class, which implements data transfer using some
 * DB abstraction layer.
 * @author Sardar Yumatov <ja.doma@gmail.com>
 */
abstract class AbstractActiveRecord {

	/** The field is an primary key (identifier), always read only.
	 * Multiple ID fields will be combined to a single primary key. */
	const ID = 0;
	/** The field is read-only (may be an expression) */
	const READ_ONLY = 1;
	/** The field is read-write capable, but may not contain null value */
	const READ_WRITE = 2;
	/** The field is read-write capable and may contain null value */
	const READ_WRITE_NULL = 3;

	/** Associative array with field data. The keys are public keys visible
	 * outside the class. */
	protected $values;
	/** All changes fields with new values. The keys are public keys visible
	 * outside this class. */
	protected $changed_values = null;
	/** Mapping of public field name to internal DB row field name, provided by subclass */
	protected $name_map;
	/** Mepping of public field name to access type (1-red only, 2-R/W, 3-R/W with null value allowed), provided by subclass */
	protected $access_types;
	/** list of fields that have ID access type */
	protected $access_ids;

	/**
	 * Initialize new abstract active record instance.
	 * Constructor accepts $field_values array wich must contain at least values
	 * for ID fields. Other fields will be initalized on demand.
	 *
	 * The $field_values is a list of (key=>value) pairs, where key is an internal database field name
	 * or an SQL expression. This class doesn't recognize keys and is using them as normal strings.
	 * You may pass the fetched record from database directly to the constructor.
	 *
	 * @param array $field_values initial values of this record, at least ID fields are expected
	 */
	public function __construct($field_values) {
		$this->name_map = $this->listDefinedFields();
		assert(is_array($this->name_map));
		$this->access_types = $this->listFieldAccessTypes();
		assert(is_array($this->access_types));

		$this->access_ids = array();
		foreach($this->access_types as $k=>$t) {
			if($t == self::ID && !isset($field_values[$this->name_map[$k]])) throw new InvalidArgumentException("Required ID field '{$k}' is not specified!");
			$this->access_ids[] = $k;
		}

		$this->values = array();
		$map = array_flip($this->name_map);
		foreach ($field_values as $k=>$val) $this->values[$map[$k]] = $val;
	}

	/**
	 * Returns true if the field $name is defined in this record.
	 * Accessing non existing fields in other methods will result in exception.
	 * @param string $name the field name
	 */
	public function fieldExists($name) {
		return isset($this->field_map[$name]);
	}

	/** Returns true if field associated with $name is an row identifier */
	public function isIdentifierField($name) {

	}

	public function isWriteable($allow_null = false) {

	}

	/**
	 * Returns field of the database record if it is accessible.
	 * Method throws exception if method is not defined (not accessible) in this
	 * record, indicating you program logic error. Method may return null if
	 * this field may contain null.
	 * @throws InvalidArgumentException logic error, unknown field name
	 * @throws DatabaseAccessFailedException if row data needs to be read on demand and this fails
	 * @param string $name the field name
	 * @return mixed the field value
	 */
	public function __get($name) {
		if(!$this->fieldExists($name)) throw new InvalidArgumentException("The field '{$name}' doesn't exists in this active row!");
		//changes are directly available
		if(is_array($this->field_changed_values) && isset($this->field_changed_values[$name])) return $this->field_changed_values[$name];
		if(is_null($this->field_values)) {
			$this->fetchRowData();
			assert(!is_null($this->field_values));
		}
		return $this->field_values[$this->field_map[$name]];
	}

	/**
	 * Change the field value.
	 * Method throws exception if field is read-only or if may not contain
	 * null while null $value is given. All changes will be directly seen by
	 * <tt>__get()</tt>, but they will not be written out until <tt>update()</tt> call.
	 * @throws InvalidArgumentException logic error, unknown field name or wrong access type
	 * @param string $name the field name
	 * @param mixed $value the new value
	 * @return void
	 */
	public function __set($name, $value) {
		if(!$this->fieldExists($name)) throw new InvalidArgumentException("The field '{$name}' doesn't exists in this active row!");
		if($this->field_access[$name] < 1 || ($this->field_access[$name] < 2 && is_null($value))) throw new InvalidArgumentException("The field '{$name}' is either read only or may not contain null value while null is provided");
		if(!is_array($this->field_changed_values)) $this->field_changed_values = array();
		$this->field_changed_values[$name] = $value;
	}

	/**
	 * Returns true if requested field is defined in this record
	 * and contains non <b>null</b> value. The values false, 0 end empty
	 * string will return true.
	 * @param string $name the field name
	 * @return boolean
	 */
	public function __isset($name) {
		return $this->fieldExists($name) && !is_null($this->__get($name));
	}

	/**
	 * Changes field value to null.
	 * Method throws exception if method may not contain null. Nothing
	 * will be done if field is not defined in this record.
	 * @throws InvalidArgumentException if field may not be set to null
	 * @param string $name the field name
	 * @return void
	 */
	public function __unset($name) {
		if(!$this->fieldExists($name)) return;
		$this->__set($name, null);
	}

	/**
	 * Update this record. This method commits all changes to associated record in the database.
	 *
	 * Actually the word (changes or the full record image) will be reported to
	 * subclass because this class doesn't handle data transfer. This method does
	 * nothing if no fields where changed.
	 *
	 * Method throws runtime exception on failure.
	 * @throws DatabaseTransactionFailedException if commit is failed
	 * @return void
	 */
	public function update() {
		if(!$this->changed) return;
		$temp = array();
		foreach($this->field_changed_values as $k=>$v) $temp[$this->field_map[$k]] = $v;
		$this->updateRowData($temp); //throws DatabaseTransactionFailedException
		//disable changes... this is not big performance loss ;-)
		foreach($this->field_changed_values as $k=>$v) $this->field_values[$k] = $v;
		$this->field_changed_values = null;
	}

	/**
	 * Returns true if any field was changed since last commit.
	 * The method returns true if subsequent call to <tt>update()</tt> will perform query.
	 * @return boolean
	 */
	public function hasPendingChanges() {
		return !is_null($this->field_changed_values);
	}


//===============- To be implemented -=========================

	/**
	 * Returns list of all defined fields as mapping of public (instance field) name to
	 * internal database row field name. The internal name may be an SQL expression if
	 * field is calculated (internal names are totaly ignored by this class).
	 * You will probably implement this method as shown in example:
	 * <code>
	 *   return array('id'=>'id', 'name' =>'short_descr', 'description'=>'long_descr');
	 * </code>
	 * @throws RuntimeException on any error
	 * @return array (public_name=>internal_DB_row_name) pairs containing field names
	 */
	protected abstract function listDefinedFields();

	/**
	 * Defines how the fields are accessible.
	 * Method should return associative array mapping access type to public name.
	 * The access type is a number:
	 *   - 0 - this field is an identifer (primary key, there can be more PK fields)
	 *   - 1 - field is read only
	 *   - 2 - field is read/write capable, but may not contain null value
	 *   - 3 - field is R/W capable and may contain null value
	 *
	 * You will probably implemnt this method as shown in example:
	 * <code>
	 *   //id is read only, name is R/W, description is optional
	 *   return array('id'=>0, 'name'=>1, 'description'=>2)
	 * </code>
	 *
	 * <b>Note:</b> do not load the schema from a file, hard-code it in your logic.
	 * It is really simple to change field list when needed instead of suffering from continous overhead ;-)
	 *
	 * @param string $field_name the public obect's field name
	 * @return integer access code [0-3]
	 */
	protected abstract function listFieldAccessTypes();


	/**
	 * This method will be called if fields of this row are accessed, but
	 * the <tt>$field_values</tt> is still unitialized. After calling this method
	 * the <tt>$field_values</tt> must contain (key=>value) pairs, where key is the internal
	 * database row key. Maping from <tt>listDefinedFields()</tt> will be used to convert
	 * public names to internal database row names.
	 *
	 * You will probably initialize <tt>$field_values</tt> from you constructor instead of waiting call
	 * to this method on demand. The value will be probably the result from some "fetch array" operation returning
	 * row as assciative array with string or nummeric keys. Fields that are not listed in
	 * array returned by <tt>listDefinedFields()</tt> will be invisible to outside world.
	 * @throws DatabaseAccessFailedException on any error
	 * @return void
	 */
	protected abstract function fetchRowData();

	/**
	 * Update database row.
	 *
	 * This method should implement actual writing out of the changed row data to database.
	 * Given $row will contain <tt>key=>value</tt> pairs where key is already mapped to
	 * internal row field names using mapping provided by <tt>listDefinedFields()</tt>
	 * Only the fields that where actually changes will be listed in $row.
	 *
	 * @param array $row the changed row fields
	 * @throws DatabaseTransactionFailedException on any error
	 * @return void
	 */
	protected abstract function updateRowData($row);
}

?>